import torch
import math
import warnings
import torch.nn as nn
import torch.nn.functional as F

from torch.nn import Parameter, init
from nets.modules.CNN import CNN

def _no_grad_trunc_normal_(tensor, mean, std, a, b):
    # Cut & paste from PyTorch official master until it's in a few official releases - RW
    # Method based on https://people.sc.fsu.edu/~jburkardt/presentations/truncated_normal.pdf
    def norm_cdf(x):
        # Computes standard normal cumulative distribution function
        return (1. + math.erf(x / math.sqrt(2.))) / 2.

    if (mean < a - 2 * std) or (mean > b + 2 * std):
        warnings.warn("mean is more than 2 std from [a, b] in nn.init.trunc_normal_. "
                      "The distribution of values may be incorrect.",
                      stacklevel=2)

    with torch.no_grad():
        # Values are generated by using a truncated uniform distribution and
        # then using the inverse CDF for the normal distribution.
        # Get upper and lower cdf values
        l = norm_cdf((a - mean) / std)
        u = norm_cdf((b - mean) / std)

        # Uniformly fill tensor with values from [l, u], then translate to
        # [2l-1, 2u-1].
        tensor.uniform_(2 * l - 1, 2 * u - 1)

        # Use inverse cdf transform for normal distribution to get truncated
        # standard normal
        tensor.erfinv_()

        # Transform to proper mean, std
        tensor.mul_(std * math.sqrt(2.))
        tensor.add_(mean)

        # Clamp to ensure it's in the proper range
        tensor.clamp_(min=a, max=b)
        return tensor

def compute_var(y):
        y = y.view(-1, y.size(-1))
        zc = torch.tensor(y.size(0)).cuda()
        zs = y.sum(dim=0)
        zss = (y ** 2).sum(dim=0)
        var = zss / (zc - 1) - (zs ** 2) / (zc * (zc - 1))
        return torch.sqrt(var + 1e-6)
    
def trunc_normal_(tensor, mean=0., std=1., a=-2., b=2.):
    return _no_grad_trunc_normal_(tensor, mean, std, a, b)

class MaskCNN(CNN):
    def __init__(self, *args, **kwargs):
        super(MaskCNN, self).__init__(*args, **kwargs)
        self.layer_wise_embedding = nn.ParameterList()
        for i in range(len(self.nb_filters)):
            layer_emd = Parameter(torch.zeros(1, 1, 1, 128 // int(math.pow(2, i))))
            trunc_normal_(layer_emd, std=.02)
            self.layer_wise_embedding.append(layer_emd)
        
    def forward(self, x, mode=None, sep_index=None, mask=None):
        # conv features
        if mask is None:
            for i, layer in enumerate(self.cnn):
                x = layer(x)
            return x
        else:
            for i, layer in enumerate(self.cnn):
                if isinstance(layer, nn.Conv2d) or isinstance(layer, nn.Conv1d):
                    B, C, H, W = x.shape
                    # norm layer_wise embedding
                    layer_emd = F.normalize(self.layer_wise_embedding[(i + 1)//5], dim=-1)
                    layer_mask = mask[(i + 1)//5]
                    temp_mask = layer_mask[:, 0, :, 0]
                    # compute var
                    state_x = x.transpose(1, 2)[temp_mask.bool()]  # BxT, H, F
                    # compute mean
                    reduce_dim = [0, 2]
                    mean = state_x.mean(reduce_dim).unsqueeze(0).unsqueeze(-1).unsqueeze(-1)
                    var = state_x.var(reduce_dim, unbiased=False).unsqueeze(0).unsqueeze(-1).unsqueeze(-1)
                    emd = layer_emd.repeat(B, C, H, 1)
                    emd = layer_emd * var + mean
                    x = x * (1 - layer_mask) + emd * layer_mask
                x = layer(x)
            return x